/* NumberType.js
 *
 * Copyright 2006, Tim Dwelle.
 *
 * Licensed under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in
 * compliance with the License.  You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in
 * writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 * CONDITIONS OF ANY KIND, either express or implied.  See
 * the License for the specific language governing
 * permissions and limitations under the License.
 *
 */

$package("dowry.datatype");

/**
 * Object representation of a number datatype.
 *
 */
$class("NumberType").$extends("Datatype").$as(
{
    /**
     * Constant for the string literal representing the
     * decimal separator.
     *
     */
    DECIMAL_SEPARATOR : Dowry.msgs.getMessage("."),

    /**
     * Constant for the error message when validation finds
     * a value that should be an integer, but contains a
     * decimal point.
     *
     */
    ERROR_DECIMAL_SEPARATOR_NOT_PERMITTED :
        Dowry.msgs.getMessage("ERROR_DECIMAL_SEPARATOR_NOT_PERMITTED"),

    /**
     * Constant for the error message when validation finds
     * a value that exceeds the maximum value.
     *
     */
    ERROR_EXCEEDS_MAXIMUM_VALUE :
        Dowry.msgs.getMessage("ERROR_EXCEEDS_MAXIMUM_VALUE"),

    /**
     * Constant for the error message when validation finds
     * a value that exceeds the minimum value.
     *
     */
    ERROR_EXCEEDS_MINIMUM_VALUE :
        Dowry.msgs.getMessage("ERROR_EXCEEDS_MINIMUM_VALUE"),

    /**
     * Constant for the error message when validation finds
     * a value that exceeds the precision (specifically,
     * when the left-side of the decimal point is too large
     * to allow for the specified scale).
     *
     */
    ERROR_EXCEEDS_PRECISION :
        Dowry.msgs.getMessage("ERROR_EXCEEDS_PRECISION"),

    /**
     * Constant for the error message when validation finds
     * a value that exceeds the scale.
     *
     */
    ERROR_EXCEEDS_SCALE :
        Dowry.msgs.getMessage("ERROR_EXCEEDS_SCALE"),

    /**
     * Constant for the error message when validation finds
     * a negative value when the datatype only allows
     * non-negative.
     *
     */
    ERROR_NEGATIVE_NOT_PERMITTED :
        Dowry.msgs.getMessage("ERROR_NEGATIVE_NOT_PERMITTED"),

    /**
     * Constant for the error message when validation finds
     * a value that does not represent a number.
     *
     */
    ERROR_NOT_A_NUMBER :
        Dowry.msgs.getMessage("ERROR_NOT_A_NUMBER"),

    /**
     * Constant for the error message when validation finds
     * a positive value when the datatype only allows
     * non-positive.
     *
     */
    ERROR_POSITIVE_NOT_PERMITTED :
        Dowry.msgs.getMessage("ERROR_POSITIVE_NOT_PERMITTED"),

    /**
     * Constant for the error message when validation finds
     * a value of zero when the datatype is configured as
     * non-zero.
     *
     */
    ERROR_ZERO_NOT_PERMITTED :
        Dowry.msgs.getMessage("ERROR_ZERO_NOT_PERMITTED"),

    /**
     * Constant for the string literal representing the
     * negative sign.
     *
     */
    NEG_SIGN : "-",

    /**
     * Constant for the string literal representing the
     * characters representing numbers.
     *
     */
    NUMERALS : "0123456789",

    /**
     * Constant for the string literal representing the
     * positive sign.
     *
     */
    POS_SIGN : "+",

    /**
     * Number property indicating the maximum allowable
     * numeric value.  The default is null (no maximum
     * numeric value).
     *
     */
    maxValue : null,

    /**
     * Number property indicating the minimum allowable
     * numeric value.  The default is null (no minimum
     * numeric value).
     *
     */
    minValue : null,

    /**
     * Boolean property indicating whether or not the number
     * cannot be positive (<code>num <= 0</code>).  The
     * default is false (the number can be positive).
     *
     */
    negative : false,

    /**
     * Boolean property indicating whether or not the number
     * cannot be zero (<code>num != 0</code>).  The default
     * is false (the number can be 0).
     *
     */
    nonZero : false,

    /**
     * Boolean property indicating whether or not the number
     * cannot be negative (<code>num >= 0</code>).  The
     * default is false (the number can be negative).
     *
     */
    positive : false,

    /**
     * Property indicating the total number of digits in the
     * unscaled value.  The default is null (no maximum
     * number of digits).
     *
     */
    precision : null,

    /**
     * Property indicating the number of digits to the right
     * of the decimal point.  The default is null (no
     * maximum scale).
     *
     */
    scale : null,

    /**
     * Convenience function to round to arbitrary number
     * of digits after the decimal point.
     *
     * @param num     the number to round
     *
     * @param digits  the number of digits to round to
     *
     * @return        the rounded number
     *
     */
    round : function(num, digits)
    {
        this._checkInternalState();

        if (typeof num == "number")
        {
            if (digits > 0)
            {
                digits = Math.pow(10, digits);
                num = num * digits;
                num = Math.round(num);
                num = num / digits;
            }
            else
            {
                num = Math.round(num);
            }
        }
        else
        {
            num = null;
        }

        return num;
    },

    /**
     * Extends Datatype's <code>toConfiguration()</code> to
     * include the extra properties <code>maxLength</code>
     * and <code>validChars</code>.  These properties are
     * included for use by interested widgets.
     *
     * @param  id    the identifier for the field associated
     *               with this datatype (optional)
     *
     * @param  val   the field value associated with this
     *               datatype (optional)
     *
     * @return       an object containing the datatype
     *               information as a map
     *
     */
    toConfiguration : function(id, val)
    {
        this._checkInternalState();

        var cfg = this.$super(arguments);

        var len = this.precision;
        var validChars = this.NUMERALS;

        // add positive and/or negative sign
        // to valid chars
        if (!this.positive)
        {
            validChars += this.NEG_SIGN;
        }
        if (!this.negative)
        {
            validChars += this.POS_SIGN;
        }

        // only calculate length if
        // we have a precision
        if (len != null)
        {
            // positive or negative sign
            len++;

            // decimal point
            if (this.scale != null && this.scale > 0)
            {
                len++;
                validChars += this.DECIMAL_SEPARATOR;
            }
        }

        cfg.maxLength = len;
        cfg.validChars = validChars;

        return cfg;
    },

    /**
     * Converts some raw value to a value of the appropriate
     * datatype.
     *
     * @param val  the original, raw value representation
     *
     * @return     the value converted to the appropriate
     *             datatype
     *
     */
    toDatatype : function(val)
    {
        this._checkInternalState();

        // only need to convert if we have
        // a value, but its not already a
        // number
        val = this.trim(val);
        if (val != null &&
            typeof val != "number")
        {
            // convert to number
            val = Number(val);

            // invalid number
            if (isNaN(val))
            {
                val = null;
            }

            // round to the appropriate scale
            else if (this.scale != null)
            {
                val = this.round(val, this.scale);
            }
        }

        return val;
    },

    /**
     * Converts some value of the appropriate datatype to
     * a string representation.
     *
     * @param val  the original value of the appropriate
     *             datatype
     *
     * @return     the value converted to a string
     *
     */
    toFormattedString : function(val)
    {
        var s = null;

        this._checkInternalState();

        val = this.trim(val);
        if (val != null)
        {
            // convert to number
            val = Number(val);
            if (!isNaN(val))
            {
                // format to the appropriate scale
                if (typeof this.scale == "number")
                {
                    val = this.round(val, this.scale);
                }

                s = String(val);
            }
        }

        return s;

    },

    /**
     * Validates that:
     *
     * <ol>
     *  <li>  the value is provided, if required as
     *        specified by map key <code>required</code>
     *  </li>
     *  <li>  the value is a number
     *  </li>
     *  <li>  the value is positive/negative/non-zero, if
     *        specified as such according to map keys
     *        <code>positive</code>, <code>negative</code>,
     *        and <code>nonZero</code>, respectively
     *  </li>
     *  <li>  the value does not exceed the precision
     *        specified by map key <code>precision</code>
     *  </li>
     *  <li>  the value does not exceed the scale specified
     *        by map key <code>scale</code>
     *  </li>
     *  <li>  the value does not contain the decimal
     *        separator, if the scale is 0
     *  </li>
     * </ol>
     *
     * @param val    a value representation to be validated
     *
     * @return       the string error message, if the
     *               input is invalid; otherwise, null
     *
     */
    validate: function(val)
    {
        // call super.validate()...
        var msg = this.$super(arguments);

        this._checkInternalState();

        if (msg == null &&
            val != null)
        {
            var num = Number(val);

            // first make sure that the input represents
            // a number
            if (isNaN(num))
            {
                msg = this.ERROR_NOT_A_NUMBER;
            }

            // if the field is supposed to only accept
            // positive, negative, or non-zero values...
            // make sure the number value is appropriate
            else if (this.positive && num < 0)
            {
                msg = this.ERROR_NEGATIVE_NOT_PERMITTED;
            }
            else if (this.negative && num > 0)
            {
                msg = this.ERROR_POSITIVE_NOT_PERMITTED;
            }
            else if (this.nonZero && num == 0)
            {
                msg = this.ERROR_ZERO_NOT_PERMITTED;
            }

            // if the field has a maximum and/or minimum
            //value
            else if (this.minValue != null &&
                     num < this.minValue)
            {
                msg = this.ERROR_EXCEEDS_MINIMUM_VALUE;
            }
            else if (this.maxValue != null &&
                     num > this.maxValue)
            {
                msg = this.ERROR_EXCEEDS_MAXIMUM_VALUE;
            }

            // we have a number, now just make sure
            // it matches the precision & scale requested
            else if (this.precision != null &&
                     this.scale != null)
            {
                var before = 0;
                var after = 0;

                // can't trust the converted version,
                // since we may have lost precision
                // in converting it to a number... so
                // instead we will evaluate the string
                // since we already know the string "is"
                // a number
                var numAsString = String(val);

                var decimal = numAsString.indexOf(this.DECIMAL_SEPARATOR);
                if (decimal == -1)
                {
                    before = numAsString.length;
                    after = 0;
                }
                else
                {
                    before = decimal;
                    after = numAsString.length - decimal - 1;
                }

                // account for positive/negative sign
                if (numAsString.indexOf(this.POS_SIGN) == 0 ||
                    numAsString.indexOf(this.NEG_SIGN) == 0)
                {
                    before--;
                }

                // ignore the leading zero in pure decimal
                if (decimal == 1 &&
                    numAsString.charAt(0) == "0")
                {
                    before--;
                }

                // check against precison
                if (before > (this.precision - this.scale))
                {
                    msg = this.ERROR_EXCEEDS_PRECISION;
                }

                // check against scale
                else if (after > this.scale)
                {
                    msg = this.ERROR_EXCEEDS_SCALE;
                }

                // make sure we don't have a decimal point
                // in a non-decimal number
                else if (this.scale == 0 &&
                         decimal > -1)
                {
                    msg = this.ERROR_DECIMAL_SEPARATOR_NOT_PERMITTED;
                }
            }
        }

        return msg;
    },

    /**
     * Checks that internal rules regarding the
     * relationships between certain object fields
     * (such as precison & scale, or positve & negative) are
     * enforced.  If the object's internal state is invalid,
     * this method will fix it when called.
     *
     */
    _checkInternalState : function()
    {
        // precision / scale
        if (this.precision != null || this.scale != null)
        {
            this.precision = Number(this.precision);
            this.scale = Number(this.scale);
        }
        else
        {
            this.precision = null;
            this.scale = null;
        }

        // make sure we haven't made it *both*
        // only positive and only negative
        if (this.positive && this.negative)
        {
            this.positive = false;
            this.negative = false;
        }
    }
});